package com.redhipps.hips.client.widget;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.core.client.GWT;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.redhipps.hips.client.controller.AutoCompleteController;
import com.redhipps.hips.client.controller.CompletionFormatter;
import com.redhipps.hips.client.controller.CompletionListener;
import com.redhipps.hips.client.controller.InlineEditController;
import com.redhipps.hips.client.io.IOService;
import com.redhipps.hips.client.io.ModelIOException;
import com.redhipps.hips.client.io.ModelReader;
import com.redhipps.hips.client.io.RequestBuilderFactory;
import com.redhipps.hips.client.io.WriterCallback;
import com.redhipps.hips.client.io.RequestBuilderFactory.Type;
import com.redhipps.hips.client.model.Context;
import com.redhipps.hips.client.model.DateTime;
import com.redhipps.hips.client.model.Doctor;
import com.redhipps.hips.client.model.DoctorConstraint;
import com.redhipps.hips.client.model.ModelRepository;
import com.redhipps.hips.client.model.Schedule;
import com.redhipps.hips.client.model.ScheduleBlock;
import com.redhipps.hips.client.model.ScheduleSolution;
import com.redhipps.hips.client.model.ScheduleSolutionBlock;

public class SchedulingView extends Composite {

  private Context context;

  private FlexTable grid;
  private ScheduleList scheduleList;
  private DoctorList doctorList;
  private DoctorConstraintList doctorConstraintList;
  private ScheduleSolutionList solutionList;
  private CalendarView calendar;
  private SchedulingViewCellRenderer cellRenderer;

  private List<Schedule> scheduleModels;
  private List<Doctor> doctorModels;
  private List<DoctorConstraint> doctorConstraintModels;
  private List<ScheduleSolution> solutionModels;
  private Panel buttonsPanel;

  private SelectionListener<Schedule> selectionListener;
  private SelectionListener<ScheduleSolution> solutionListener;

  private final RequestBuilderFactory requestBuilderFactory;
  private final IOService ioService;

  private AutoCompleteController<Doctor> solutionLabelsCompletionCtr;
  private InlineEditController solutionLabelsEditCtr;

  public SchedulingView(Context context, IOService ioService,
      RequestBuilderFactory requestBuilderFactory) {
    this.context = context;
    this.ioService = ioService;
    this.requestBuilderFactory = requestBuilderFactory;
    this.solutionLabelsEditCtr = new InlineEditController();
    grid = new FlexTable();
    grid.setWidth("100%");
    grid.getFlexCellFormatter().setRowSpan(0, 0, 4);
    buttonsPanel = createButtonsPanel();
    grid.setWidget(3, 0, buttonsPanel);
    grid.getCellFormatter().setAlignment(0, 0,
        HasHorizontalAlignment.ALIGN_LEFT,
        HasVerticalAlignment.ALIGN_TOP);
    grid.getCellFormatter().setAlignment(0, 1,
        HasHorizontalAlignment.ALIGN_LEFT,
        HasVerticalAlignment.ALIGN_TOP);
    grid.getCellFormatter().setAlignment(1, 1,
        HasHorizontalAlignment.ALIGN_LEFT,
        HasVerticalAlignment.ALIGN_TOP);
    grid.getCellFormatter().setAlignment(2, 0,
        HasHorizontalAlignment.ALIGN_CENTER,
        HasVerticalAlignment.ALIGN_TOP);
    grid.getCellFormatter().setAlignment(3, 0,
        HasHorizontalAlignment.ALIGN_RIGHT,
        HasVerticalAlignment.ALIGN_TOP);
    initWidget(grid);
  }

  private Panel createButtonsPanel() {
    HorizontalPanel panel = new HorizontalPanel();
    Button saveButton = new Button("Verify Assignment");
    saveButton.addClickListener(new VerifyAssignmentListener());
    panel.add(saveButton);
    Button assignButton = new Button("Assign Doctors in Schedule");
    assignButton.addClickListener(new AssignDoctorsListener());
    panel.add(assignButton);
    return panel;
  }

  public void setScheduleSelectionListener(SelectionListener<Schedule> l) {
    this.selectionListener = l;
  }

  public void setScheduleSolutionSelectionListener(SelectionListener<ScheduleSolution> l) {
    this.solutionListener = l;
  }

  public void selectSchedule(Schedule schedule) {
    //context.pushSchedule(schedule);
    setCalendar(schedule);
    if (selectionListener != null) {
      selectionListener.itemSelected(schedule, this);
    }
  }

  public void setSchedules(List<Schedule> schedules) {
    scheduleModels = schedules;
    scheduleList = new ScheduleList(context, scheduleModels);
    scheduleList.setItemSelectionListener(new SelectionListener<Schedule>() {
      public void itemSelected(Schedule model, Widget widget) {
        selectSchedule(model);
      }
    });
    grid.setWidget(0, 0, scheduleList);
    if (schedules.size() > 0) {
      setCalendar(schedules.get(0));
    }
  }

  public void setDoctors(List<Doctor> doctors) {
    doctorModels = doctors;
    doctorList = new DoctorList(context, doctorModels);
    grid.setWidget(0, 1, doctorList);
  }

  public void setDoctorConstraints(List<DoctorConstraint> constraints) {
    doctorConstraintModels = constraints;
    doctorConstraintList = new DoctorConstraintList(context, constraints,
        new ModelRepository<Doctor>(doctorModels), ioService);
    // Replace the doctor list.
    grid.setWidget(0, 1, doctorConstraintList);
    doctorList = null;
  }

  public void setCalendar(Schedule schedule) {
    DateTime startTime = schedule.getStartTime();
    cellRenderer = new SchedulingViewCellRenderer();
    calendar = new CalendarView(startTime, cellRenderer);
    grid.setWidget(2, 0, calendar);
  }

  public void selectSolution(ScheduleSolution solution) {
    if (solutionListener != null) {
      solutionListener.itemSelected(solution, this);
    }
  }

  public void setSolutions(List<ScheduleSolution> solutions) {
    solutionModels = solutions;
    solutionList = new ScheduleSolutionList(context, solutionModels);
    solutionList.setItemSelectionListener(new SelectionListener<ScheduleSolution>() {
      public void itemSelected(ScheduleSolution model, Widget widget) {
        selectSolution(model);
      }
    });
    grid.setWidget(1, 0, solutionList);
  }

  public void setSolution(ScheduleSolution solution) {
    ModelRepository<Doctor> docs = new ModelRepository<Doctor>(
        context.institution().getDoctors());
    ModelRepository<ScheduleBlock> blocks = new ModelRepository<ScheduleBlock>(
        context.schedule().getScheduleBlocks());
    solutionLabelsCompletionCtr = createDoctorAutocompleteCtr(docs);
    cellRenderer.setSolution(new SolutionHelper(solution, docs, blocks));
    calendar.redraw();
  }

  private static CompletionFormatter<Doctor> DOCTOR_FORMATTER =
      new CompletionFormatter<Doctor>() {
        public String format(Doctor value) {
          return value.getName();
        }
      };

  private static AutoCompleteController<Doctor> createDoctorAutocompleteCtr(
      ModelRepository<Doctor> docs) {
    Map<String, Doctor> map = new HashMap<String, Doctor>();
    for (Doctor d : docs) {
      String[] parts = d.getName().split(" ");
      for (String s : parts) {
        map.put(s.toLowerCase(), d);
      }
    }
    return new AutoCompleteController<Doctor>(map, DOCTOR_FORMATTER);
  }

  private class SolutionHelper {

    ScheduleSolution solution;
    List<List<Doctor>> shiftToDocs;
    List<List<ScheduleSolutionBlock>> scheduleSolutionBlocks;

    public SolutionHelper(ScheduleSolution solution, ModelRepository<Doctor> docs,
        ModelRepository<ScheduleBlock> scheduleBlocks) {
      this.solution = solution;
      shiftToDocs = new ArrayList<List<Doctor>>(scheduleBlocks.size());
      for (int i = 0; i < scheduleBlocks.size(); i++) {
        shiftToDocs.add(new ArrayList<Doctor>());
      }
      scheduleSolutionBlocks = new ArrayList<List<ScheduleSolutionBlock>>();
      for (int i = 0; i < scheduleBlocks.size(); i++) {
        scheduleSolutionBlocks.add(new ArrayList<ScheduleSolutionBlock>());
      }
      
      List<ScheduleSolutionBlock> existingBlocks = solution.getScheduleSolutionBlocks();
      for (ScheduleSolutionBlock solutionBlock : existingBlocks) {
        Doctor doctor = docs.get(solutionBlock.getDoctorRef());
        ScheduleBlock block = scheduleBlocks.get(solutionBlock.getBlockRef());
        DateTime date = block.getStartTime();
        int index = (date.getDay() - 1) * 2;
        if (date.getHour() > 12) {
          index += 1;
        }
        shiftToDocs.get(index).add(doctor);  // TODO Sorting
        scheduleSolutionBlocks.get(index).add(solutionBlock);
      }
    }

    public ScheduleSolution solution() {
      return solution;
    }

    public List<Doctor> doctorsForShift(int i) {
      return shiftToDocs.get(i);
    }

    public List<ScheduleSolutionBlock> solutionBlocksForShift(int i) {
      return scheduleSolutionBlocks.get(i);
    }

    public int size() {
      return shiftToDocs.size();
    }
  }

  private class SchedulingViewCellRenderer implements CalendarCellRenderer {

    private SolutionHelper solution;
    private WriterCallback<ScheduleSolutionBlock> writerCallback;

    public SchedulingViewCellRenderer() {
      solution = null;
      writerCallback = new WriterCallback<ScheduleSolutionBlock>() {
        public void onFailure(Throwable caught) {
          GWT.log("ScheduleSolutionBlock write failed", caught);
        }
        public void onSuccess(List<ScheduleSolutionBlock> result) {
          ModelRepository<Doctor> docs = new ModelRepository<Doctor>(
              context.institution().getDoctors());
          GWT.log("New doctor: " + docs.get(result.get(0).getDoctorRef()).getName(), null);
        }
      };
    }

    public void setSolution(SolutionHelper solution) {
      this.solution = solution;
    }

    private void addLabelsForShift(VerticalPanel panel, int shift, String... labelStyles) {
      List<Doctor> day = solution.doctorsForShift(shift);
      List<ScheduleSolutionBlock> solutionBlocksAm =
          solution.solutionBlocksForShift(shift);
      for (int i = 0; i < day.size(); i++) {
        Doctor doc = day.get(i);
        final ScheduleSolutionBlock block = solutionBlocksAm.get(i);
        Label dayShiftDoctorName = labelWithStyles(doc.getName(), labelStyles);
        Widget wrappedLabel = solutionLabelsEditCtr.wrap(
            dayShiftDoctorName, solutionLabelsCompletionCtr,
            new CompletionListener<Doctor>() {
              public void valueCompleted(String formatted, Doctor value) {
                block.setDoctorRef(value.key());
                context.pushScheduleSolution(solution.solution());
                ioService.write(context, block, writerCallback);
              }
            });
        panel.add(wrappedLabel);
      }
    }

    public Widget render(DateTime date) {
      int d = date.getDay();
      int firstShift = (d - 1) * 2;
      int secondShift = firstShift + 1;

      VerticalPanel singleDayPanel = new VerticalPanel();
      singleDayPanel.setStyleName("daypanel");
      // TODO Define in CSS somewhere
      singleDayPanel.setSpacing(2);
      singleDayPanel.add(labelWithStyles(String.valueOf(d), "daynumber"));
    
      if (solution != null && (firstShift + 1) < solution.size()) {
        singleDayPanel.add(labelWithStyles("AM:", "am", "shiftname"));
        addLabelsForShift(singleDayPanel, firstShift, "am", "doctor");
        singleDayPanel.add(labelWithStyles("PM:", "pm", "shiftname"));
        addLabelsForShift(singleDayPanel, secondShift, "pm", "doctor");
      }

      return singleDayPanel;
    }
    
    private Label labelWithStyles(String text, String... styles) {
      Label label = new Label(text);
      for (String s : styles) {
        label.addStyleName(s);
      }
      return label;
    }
  }

  // Schedule creation

  private class AssignDoctorsListener implements ClickListener {
    
    public void onClick(Widget sender) {
      RequestBuilder createScheduleRequest = requestBuilderFactory.createBuilder(
          Type.ASSIGN_DOCTORS, context);
      try {
        createScheduleRequest.sendRequest(null, new RequestCallback() {
          public void onError(Request request, Throwable exception) {
            GWT.log("Scheduling request failed.", exception);
          }
          public void onResponseReceived(Request request, Response response) {
            ModelReader<ScheduleSolution> reader = ioService.reader(ScheduleSolution.class);
            JSONArray array = JSONParser.parse(response.getText()).isArray();
            try {
              ScheduleSolution solution = reader.readFully(array);
              setSolution(solution);
            } catch (ModelIOException e) {
              GWT.log("Error parsing schedule solution", e);
            }
          }
        });
      } catch (RequestException e) {
        GWT.log("Error sending scheduling request", e);
      }
    }

  }

  // Schedule saving

  private class VerifyAssignmentListener implements ClickListener {

    public void onClick(Widget sender) {
      Dialogs.debug("Not implemented.");
    }
    
  }

}
